package com.gx.service.impl;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.MD5;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gx.common.exception.FileUploadException;
import com.gx.common.result.AppResult;
import com.gx.mapper.FilesMapper;
import com.gx.model.dto.QueryMediaParamsDto;
import com.gx.model.dto.UploadFileParamsDto;
import com.gx.model.po.TbFiles;
import com.gx.model.result.UploadFileResultDto;
import com.gx.service.FileService;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author Liu-pj
 * @Date 2023/3/7 17:20
 * @Desc 文件操作
 **/
@Slf4j
@Service
public class FileServiceImpl implements FileService {
    @Autowired
    FilesMapper filesMapper;
    @Autowired
    MinioClient minioClient;
    @Autowired
    FileService currentProxy;
    @Value("${minio.url}")
    private String url;
    @Value("${minio.bucket.minFiles}")
    private String minFilesBucket;
    @Value("${minio.bucket.maxFiles}")
    private String maxFilesBucket;

    @Override
    public Page<TbFiles> queryMediaFiles(QueryMediaParamsDto queryMediaParamsDto) {
        String fileType = queryMediaParamsDto.getFileType();
        String filename = queryMediaParamsDto.getFilename();
        //构建查询条件对象
        LambdaQueryWrapper<TbFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StrUtil.isNotBlank(fileType), TbFiles::getFileType, fileType);
        queryWrapper.like(StrUtil.isNotBlank(filename), TbFiles::getFileName, filename);
        //分页对象
        Page<TbFiles> page = new Page<>(queryMediaParamsDto.getPageNo(), queryMediaParamsDto.getPageSize());
        return filesMapper.selectPage(page, queryWrapper);

    }

    @Override
    public TbFiles getFileById(String id) {
        TbFiles tbFiles = filesMapper.selectById(id);
        if (tbFiles == null) {
            FileUploadException.cast("文件不存在");
        }
        String url = tbFiles.getUrl();
        if (StrUtil.isEmpty(url)) {
            FileUploadException.cast("文件还没有处理，请稍后预览");
        }
        return tbFiles;
    }

    @Override
    public UploadFileResultDto uploadFile(UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
        //得到文件的md5值
        String fileMd5 = MD5.create().digestHex(bytes);
        if (StrUtil.isEmpty(folder)) {
            //自动生成目录的路径 按年月日生成，
            folder = getFileFolder(true, true, true);
        } else if (!folder.contains("/")) {
            folder = folder + "/";
        }
        //文件名称
        String filename = uploadFileParamsDto.getFileName();

        if (StrUtil.isEmpty(objectName)) {
            //如果objectName为空，使用文件的md5值为objectName
            objectName = fileMd5 + filename.substring(filename.lastIndexOf("."));
        }
        objectName = folder + objectName;
        try {
            addMediaFilesToMinio(bytes, minFilesBucket, objectName);
            TbFiles tbFiles = currentProxy.addMediaFilesToDb(fileMd5, uploadFileParamsDto, minFilesBucket, objectName);
            //准备返回数据
            UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
            BeanUtils.copyProperties(tbFiles, uploadFileResultDto);
            return uploadFileResultDto;
        } catch (Exception e) {
            log.debug("上传文件失败：{}", e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public AppResult<Integer> checkChunk(String fileMd5, Integer totalChunk) {
        //判断文件是否存在
        Boolean checkFileResult = this.checkFile(fileMd5);
        if (BooleanUtil.isTrue(checkFileResult)) {
            //已完结,已合并
            return AppResult.success(-1);
        }
        //得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        for (int i = 0; i < totalChunk; i++) {
            //分块文件的路径
            String chunkFilePath = chunkFileFolderPath + i;
            Boolean result = checkFileExit(chunkFilePath);
            if (BooleanUtil.isFalse(result)) {
                //待续传
                return AppResult.success(i);
            }
        }
        //已完结,待合并
        return AppResult.success(-2);
    }

    @Override
    public AppResult<Boolean> uploadChunk(String fileMd5, int chunk, byte[] bytes) {
        //得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;

        try {
            //将分块上传到文件系统
            addMediaFilesToMinio(bytes, maxFilesBucket, chunkFilePath);
            //上传成功
            return AppResult.success(true);
        } catch (Exception e) {
            log.debug("上传分块文件失败：{}", e.getMessage());
            return AppResult.failure("上传分块失败", 500);
        }

    }

    /**
     * 合并分块
     *
     * @param uploadFileParamsDto 文件信息
     * @return 结果
     */
    @Override
    public AppResult<Boolean> mergeChunks(UploadFileParamsDto uploadFileParamsDto) {
        String fileMd5 = uploadFileParamsDto.getFileMd5();
        Integer chunkTotal = uploadFileParamsDto.getChunkTotal();
        //下载分块
        File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
        //得到合并后文件的扩展名
        String fileName = uploadFileParamsDto.getFileName();
        //扩展名
        String extension = fileName.substring(fileName.lastIndexOf("."));
        File tempMergeFile = null;
        try {
            try {
                //创建一个临时文件作为合并文件
                tempMergeFile = File.createTempFile("merge", extension);
                String path = tempMergeFile.getPath();
                log.info("临时文件路径:{}", path);
            } catch (IOException e) {
                FileUploadException.cast("创建临时合并文件出错");
            }
            //创建合并文件的流对象
            try (RandomAccessFile raf_write = new RandomAccessFile(tempMergeFile, "rw")) {
                byte[] b = new byte[1024];
                for (File file : chunkFiles) {
                    //读取分块文件的流对象
                    try (RandomAccessFile rafRead = new RandomAccessFile(file, "r");) {
                        int len = -1;
                        while ((len = rafRead.read(b)) != -1) {
                            //向合并文件写数据
                            raf_write.write(b, 0, len);
                        }
                    }
                }
            } catch (IOException e) {
                FileUploadException.cast("合并文件过程出错");
            }
            //校验合并后的文件是否正确
            try (FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);) {
                String mergeMd5Hex = DigestUtil.md5Hex(mergeFileStream);
                if (!fileMd5.equals(mergeMd5Hex)) {
                    log.debug("合并文件校验不通过,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);
                    FileUploadException.cast("合并文件校验不通过");
                }
            } catch (IOException e) {
                log.debug("合并文件校验出错,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);
                FileUploadException.cast("合并文件校验出错");
            }
            //拿到合并文件在minio的存储路径
            String mergeFilePath = getFilePathByMd5(fileMd5, extension);
            //将合并后的文件上传到文件系统
            addMediaFilesToMinio(tempMergeFile.getAbsolutePath(), maxFilesBucket, mergeFilePath);
            //将文件信息入库保存
            uploadFileParamsDto.setFileSize(tempMergeFile.length());
            addMediaFilesToDb(fileMd5, uploadFileParamsDto, maxFilesBucket, mergeFilePath);
            return AppResult.success(true);
        } finally {
            //删除临时分块文件
            for (File chunkFile : chunkFiles) {
                if (chunkFile.exists()) {
                    chunkFile.delete();
                }
            }
            //删除合并的临时文件
            if (tempMergeFile != null) {
                tempMergeFile.delete();
            }
        }
    }

    public TbFiles addMediaFilesToDb(String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
        //保存到数据库
        TbFiles tbFiles = filesMapper.selectById(fileId);
        if (tbFiles == null) {
            tbFiles = new TbFiles();
            //封装数据
            BeanUtils.copyProperties(uploadFileParamsDto, tbFiles);
            String contentType = uploadFileParamsDto.getContentType();
            if (contentType.contains("image")) {
                tbFiles.setFileType("1");
            } else if (contentType.contains("video")) {
                tbFiles.setFileType("2");
            } else {
                tbFiles.setFileType("3");
            }
            tbFiles.setContentType(contentType);
            tbFiles.setId(fileId);
            tbFiles.setBucket(bucket);
            tbFiles.setFilePath(objectName);
            tbFiles.setUrl(url + "/" + bucket + "/" + objectName);
            //插入文件表
            filesMapper.insert(tbFiles);
        }
        return tbFiles;
    }

    public void downloadFileFromMinio(File file, String bucket, String objectName) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
        try (
                InputStream inputStream = minioClient.getObject(getObjectArgs);
                FileOutputStream outputStream = new FileOutputStream(file);
        ) {
            IOUtils.copy(inputStream, outputStream);
        } catch (Exception e) {
            e.printStackTrace();
            FileUploadException.cast("查询分块文件出错");
        }
    }

    public void addMediaFilesToMinio(String filePath, String bucket, String objectName) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .filename(filePath)
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("文件上传成功:{}", filePath);
        } catch (Exception e) {
            FileUploadException.cast("文件上传到文件系统失败");
        }
    }

    /**
     * 校验文件是否存在
     *
     * @param fileMd5 分片路径
     * @return 结果
     */
    private Boolean checkFile(String fileMd5) {
        //在文件表存在，并且在文件系统存在，此文件才存在
        TbFiles tbFiles = filesMapper.selectById(fileMd5);
        if (tbFiles == null) {
            return false;
        }
        //查看是否在文件系统存在
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(tbFiles.getBucket()).object(tbFiles.getFilePath()).build();
        try (InputStream inputStream = minioClient.getObject(getObjectArgs);) {
            if (inputStream == null) {
                //文件不存在
                return false;
            }
        } catch (Exception e) {
            //文件不存在
            return false;
        }
        //文件已存在
        return true;
    }

    /**
     * 校验分片文件是否存在
     *
     * @param chunkFilePath 分片路径
     * @return 结果
     */
    private Boolean checkFileExit(String chunkFilePath) {
        //查询文件系统分块文件是否存在
        //查看是否在文件系统存在
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(maxFilesBucket)
                .object(chunkFilePath)
                .build();
        try (InputStream inputStream = minioClient.getObject(getObjectArgs);) {
            if (inputStream == null) {
                //文件不存在
                return false;
            }
        } catch (Exception e) {
            //文件不存在
            return false;
        }
        return true;
    }

    /**
     * 将文件上传到分布式文件系统
     *
     * @param bytes      字节
     * @param bucket     桶
     * @param objectName 路径
     */
    private void addMediaFilesToMinio(byte[] bytes, String bucket, String objectName) {
        //资源的媒体类型
        //默认未知二进制流
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (objectName.contains(".")) {
            //取objectName中的扩展名
            String extension = objectName.substring(objectName.lastIndexOf("."));
            ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
            if (extensionMatch != null) {
                contentType = extensionMatch.getMimeType();
            }
        }
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);) {
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    //InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T，最多10000)
                    .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                    .contentType(contentType)
                    .build();
            //上传到minio
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("上传文件到文件系统出错:{}", e.getMessage());
            FileUploadException.cast("上传文件到文件系统出错");
        }
    }

    /**
     * 下载分块
     *
     * @param fileMd5    文件MD5
     * @param chunkTotal 文件总块数
     * @return 文件数组
     */
    private File[] checkChunkStatus(String fileMd5, int chunkTotal) {
        //得到分块文件所在目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //分块文件数组
        File[] chunkFiles = new File[chunkTotal];
        //开始下载
        for (int i = 0; i < chunkTotal; i++) {
            //分块文件的路径
            String chunkFilePath = chunkFileFolderPath + i;
            //分块文件
            File chunkFile = null;
            try {
                chunkFile = File.createTempFile("chunk", null);
            } catch (IOException e) {
                e.printStackTrace();
                FileUploadException.cast("创建分块临时文件出错" + e.getMessage());
            }
            //下载分块文件
            downloadFileFromMinio(chunkFile, maxFilesBucket, chunkFilePath);
            chunkFiles[i] = chunkFile;
        }
        return chunkFiles;
    }

    /**
     * 根据扩展名拿匹配的媒体类型
     *
     * @param extension 文件后缀
     * @return 文件类型
     */
    private String getMimeTypeByExtension(String extension) {
        //资源的媒体类型
        //默认未知二进制流
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (StrUtil.isNotEmpty(extension)) {
            ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
            if (extensionMatch != null) {
                contentType = extensionMatch.getMimeType();
            }
        }
        return contentType;
    }

    /**
     * 根据日期拼接目录
     *
     * @param year  年
     * @param month 约
     * @param day   天
     * @return 目录
     */
    private String getFileFolder(boolean year, boolean month, boolean day) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        //获取当前日期字符串
        String dateString = sdf.format(new Date());
        //取出年、月、日
        String[] dateStringArray = dateString.split("-");
        StringBuffer folderString = new StringBuffer();
        if (year) {
            folderString.append(dateStringArray[0]);
            folderString.append("/");
        }
        if (month) {
            folderString.append(dateStringArray[1]);
            folderString.append("/");
        }
        if (day) {
            folderString.append(dateStringArray[2]);
            folderString.append("/");
        }
        return folderString.toString();
    }

    /**
     * 合并文件在minio的存储路径
     *
     * @param fileMd5 md5
     * @param fileExt 后缀
     * @return 路径
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

    /**
     * 得到分块文件的目录
     *
     * @param fileMd5 MD5
     * @return 目录
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }


    private Boolean bucketExists(String bucketName) {
        Boolean found;
        try {
            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return found;
    }
}
